package server.order;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.LinkedList;

import server.Logger;
import server.db.DataBase;
import server.price.PriceController;

public class OrderController {
	private HashMap<String, LinkedList<Order>> openOrders;
	private DataBase db;
	private String[] currRatios;
	
	/**
	 * Construct a new OrderController
	 */
	private OrderController() {
		openOrders = new HashMap<String, LinkedList<Order>>();
		db = DataBase.getInstance();
		initOpenOrders();
		try {
			fillOpenOrders();
		} catch (SQLException e) {
			Logger.getInstance().addError("OrderController could not get open orders from the database", 1);
			//e.printStackTrace();
		}
		Logger.getInstance().addLog("OrderController has started", 1);
	}
	
	/**
	 * Create linked lists for all currency combinations
	 */
	private final void initOpenOrders() {
		String[] currs = PriceController.getInstance().getCurrencies();
		int length = ((currs.length*currs.length)-currs.length)/2;
		currRatios = new String[length];
		
		HashMap<String, Integer> doneCurrs = new HashMap<String, Integer>();
		
		int currCounter = 0;
		for (String cur1 : currs) {
			for (String cur2 : currs) {
				if (!cur1.equals(cur2) && !doneCurrs.containsKey(cur2+cur1)) {
					Logger.getInstance().addLog("New linked list for "+cur1+cur2+" orders", 4);
					openOrders.put(cur1+cur2, new LinkedList<Order>());
					doneCurrs.put(cur1+cur2, 1);
					currRatios[currCounter++] = cur1+cur2;
				}
			}
		}
	}
	
	/**
	 * Gets all open orders from the database and puts them in memory
	 * @throws SQLException
	 */
	private final void fillOpenOrders() throws SQLException {
		String query = "SELECT * FROM "+Order.TABLE+" WHERE "+Order.CLOSE_TIME+"=0 ORDER BY "+Order.OPEN_TIME+" ASC";
		ResultSet rs = db.doQuery(query);
		
		while (rs.next()) {
			Order tmp;
			switch (rs.getInt(Order.O_TYPE)) {
			case Order.ORD_MARKET:
			case Order.ORD_LIMIT:
				tmp = new LimitOrder(rs);
				break;
			case Order.ORD_TRAILING_STOP:
				tmp = new TrailingStopOrder(rs);
				break;
			default:
				tmp = null;
				break;
			}
			
			if (tmp != null)
				addOrder(tmp, false);
		}
	}
	
	public boolean addOrder(Order order) throws SQLException {
		return addOrder(order, true);
	}
	
	/**
	 * Adds an order into server memory
	 * @param order		The order to be added
	 * @param fulfill	If true, the server will try to satisfy the order after being added
	 * @return			True if the order was successfully added
	 * @throws SQLException
	 */
	private boolean addOrder(Order order, boolean fulfill) throws SQLException {
		boolean added = true;
		
		if (openOrders.containsKey(order.getCur1()+order.getCur2())) {
			openOrders.get(order.getRatio()).add(order);
		} else if (openOrders.containsKey(order.getCur2()+order.getCur1())) {
			order.switchCurs();
			openOrders.get(order.getRatio()).add(order);
		} else
			added = false;
		
		if (fulfill && added)
			fulfillOrders();
		
		if (added)
			Logger.getInstance().addLog("Added order, id = "+order.getID(), 4);
		
		return added;
	}
	
	/**
	 * Remove an order from server memory
	 * @param order		The order to be removed
	 * @return			True if the order was removed
	 */
	public boolean removeOrder(Order order) {
		if (openOrders.containsKey(order.getRatio())) {
			for(Order listOrder : openOrders.get(order.getRatio())) {
				if (listOrder.getID() == order.getID())
					return openOrders.get(order.getRatio()).remove(listOrder);
			}
			return false;
		} else {
			order.switchCurs();
			if (openOrders.containsKey(order.getRatio())) {
				for(Order listOrder : openOrders.get(order.getRatio())) {
					if (listOrder.getID() == order.getID())
						return openOrders.get(order.getRatio()).remove(listOrder);
				}
				return false;
			} else
				return false;
		}
	}
	
	/**
	 * Cycle through all currency ratio trying to fulfill orders.  If an order if fulfilled the loop is broken and started again.
	 * @throws SQLException 
	 */
	public void fulfillOrders() throws SQLException {
		boolean breakFlag = false;
		
		for (String ratio : currRatios) {
			breakFlag = false;
			for (Order order1 : openOrders.get(ratio)) {
				for (Order order2 : openOrders.get(ratio)) {
					if (order1.getID() != order2.getID() && order1.getUserId() != order2.getUserId() && order1.canSatisfy(order2)) {
						if (order1.getAmount() >= order2.getAmount())
							processOrders(order1, order2);
						else
							processOrders(order2, order1);
						
						breakFlag = true;
						break;
					}
				}
				if (breakFlag)
					break;
			}
		}
		
		if (breakFlag)
			fulfillOrders();
	}
	
	/**
	 * Closes the two orders.  Order1 amount is greater than or equal to order2 amount.
	 * @param order1		The First order (amount greater than the second order)
	 * @param order2		The second order
	 * @throws SQLException
	 */
	private void processOrders(Order order1, Order order2) throws SQLException {
		int index = openOrders.get(order1.getRatio()).indexOf(order1);
		float newAmount = order1.getAmount() - order2.getAmount();

		
		if (newAmount > 0) {
			Order openSplit, closedSplit;
			boolean switched = false;
			if (order1.isSwitched()) {
				order1.switchCurs();
				switched = true;
			}
			
			switch (order1.getOType()) {
			case Order.ORD_MARKET:
			case Order.ORD_LIMIT:
				closedSplit = new LimitOrder(order1.getCur1(), order1.getCur2(), order1.getRate(), order2.getAmount(), order1.getBType(), order1.getUserId(), order1.getID(), switched);
				openSplit = new LimitOrder(order1.getCur1(), order1.getCur2(), order1.getRate(), newAmount, order1.getBType(), order1.getUserId(), order1.getID(), switched);
				openOrders.get(openSplit.getRatio()).add(index, openSplit);
				closedSplit.openOrder(order1.getOpenTime());
				openSplit.openOrder(order1.getOpenTime());
				break;
			case Order.ORD_TRAILING_STOP:
				openSplit = new TrailingStopOrder(order1.getCur1(), order1.getCur2(), order2.getAmount(), order1.getBType(), order1.getUserId(), ((TrailingStopOrder) order1).getTrailPercent());
				closedSplit = new TrailingStopOrder(order1.getCur1(), order1.getCur2(), newAmount, order1.getBType(), order1.getUserId(), ((TrailingStopOrder) order1).getTrailPercent());;
				openOrders.get(openSplit.getRatio()).add(index, openSplit);
				closedSplit.openOrder(order1.getOpenTime());
				openSplit.openOrder(order1.getOpenTime());
				break;
			default:
				closedSplit = null;
				break;
			}
			
			if (switched)
				order1.switchCurs();
			
			order1.split();
			closedSplit.closeOrder();
		} else {
			order1.closeOrder();
		}
		
		order2.closeOrder();

		try {
			openOrders.get(order1.getRatio()).remove(order1);
		} catch (NullPointerException e) {
			Logger.getInstance().addError("Null pointer when trying to remove order id = "+order1.getID(), 3);
		}
		
		try {
			openOrders.get(order2.getRatio()).remove(order2);
		} catch (NullPointerException e) {
			Logger.getInstance().addError("Null pointer when trying to remove order id = "+order2.getID(), 3);
		}
		
		if (!order1.getRatio().equals(order2.getRatio()))
			order2.switchCurs();
		
		if (order1.getBType() == Order.BUY)
			PriceController.getInstance().getPrice(order1.getCur1(), order1.getCur2()).updatePrice(order1.getRate(), order2.getRate());
		else
			PriceController.getInstance().getPrice(order1.getCur1(), order1.getCur2()).updatePrice(order2.getRate(), order1.getRate());
	}
	
	/**
	 * Holds the only instance of OrderController
	 */
	private static class OrderControllerHolder { 
		private final static OrderController INSTANCE = new OrderController();
	}
 
	/**
	 * Reference to OrderController
	 * @return Instance of OrderController
	 */
	public static OrderController getInstance() {
		return OrderControllerHolder.INSTANCE;
	}
}
